/+
Library dbapi - Simple database api for the D programming language.

Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.base;

import std.boxer;
import std.string : str = toString;

/**
 * The structure returned by the .description property.
 */
public struct ColumnAttributes {
   char[]   name;
   char[]   typeCode;
   int      displaySize    = -1;
   int      internalSize   = -1;
   int      precision      = -1;
   int      scale          = -1;
   int      nullOk         = -1;
}

/**
 * Exception raised for important warnings like data
 * truncations while inserting, etc.
 */
public class SqlWarning : Exception {
   this (char[] msg) {super (msg);}
}

/**
 * Exception that is the base class of all other error
 * exceptions. You can use this to catch all errors with one
 * single 'except' statement. Warnings are not considered
 * errors and thus should not use this class as base.
 */
public class SqlException : Exception {
   this (char[] msg) {
      super (msg);

      m_sqlState   = null;
      m_vendorCode = 0;
   }

   this (char[] msg, char[] sqlState, int vendorCode) {
      super (msg);

      m_sqlState   = sqlState;
      m_vendorCode = vendorCode;
   }

   char[] sqlState () {
      return m_sqlState;
   }

   int vendorCode () {
      return m_vendorCode;
   }

   protected char[] m_sqlState;
   protected int    m_vendorCode;
}

private template ExceptionConstructor () {
   this (char[] msg) {super (msg);}
   this (char[] msg, char[] sqlState, int vendorCode) {super (msg, sqlState, vendorCode);}
}

/**
 * Exception raised for errors that are related to the
 * database interface rather than the database itself.
 */
public class SqlInterfaceException : SqlException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised for errors that are related to the
 * database.
 */
public class SqlDatabaseException : SqlException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised for errors that are due to problems with
 * the processed data like division by zero, numeric value
 * out of range, etc.
 */
public class SqlDataException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised for errors that are related to the
 * database's operation and not necessarily under the control
 * of the programmer, e.g. an unexpected disconnect occurs,
 * the data source name is not found, a transaction could not
 * be processed, a memory allocation error occurred during
 * processing, etc. 
 */
public class SqlOperationalException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised when the relational integrity of the
 * database is affected, e.g. a foreign key check fails.
 */
public class SqlIntegrityException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised when the database encounters an internal
 * error, e.g. the cursor is not valid anymore, the
 * transaction is out of sync, etc.
 */
public class SqlInternalException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised for programming errors, e.g. table not
 * found or already exists, syntax error in the SQL
 * statement, wrong number of parameters specified, etc.
 */
public class SqlProgrammingException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/**
 * Exception raised in case a method or database API was used
 * which is not supported by the database, e.g. requesting a
 * .rollback() on a connection that does not support
 * transaction or has transactions turned off.
 */
public class SqlNotSupportedException : SqlDatabaseException {
   mixin ExceptionConstructor;
}

/+
unittest {
   SqlWarning 

   printf ("Hello World Test\n");
   //assert (false, "an" ~ " error message");
}
+/

/**
 * Determine if a box value is the null value.
 *
 * This is necessary as the opEquals of Box does not handle nulls gracefully.
 *
 * We use box(null) for a null value instead of an empty box as empty boxes are
 * not easy to work with. For example, you would not be able to use boxArray.
 */
bool isNull (Box value) {
   return  (typeid(void*) is value.type && *cast(void**) value.data is null);
}

/**
 * Unbox a null value into a primitive.
 *
 * Primitive values are types that do not have the concept of a null value. In
 * other words, you cannot assign them the 'null' constant.
 *
 * This is a problem when the database value returned is null but we are
 * expecting, for example, an int.
 *
 * For non-char types, this function will return the initial value, defined by
 * the .init property.
 *
 * For char, dchar and wchar, a zero is returned.
 */
private template unboxNullablePrimitive (T) {
   T unboxNullablePrimitive (Box t) {
      if (isNull (t))
         static if (is (T == char) || is (T == dchar) || is (T == wchar)) {
            return cast(T)(0);
         } else {
            return typeof(T).init;
         }
      else
         return unbox!(T)(t);
   }
}

template unboxNullable (T) {
   T unboxNullable (Box t) {return unbox!(T) (t);}
}

template unboxNullable (T : byte) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : ubyte) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : short) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : ushort) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : int) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : uint) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : long) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : ulong) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : float) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : double) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : real) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : cfloat) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : cdouble) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : creal) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : ifloat) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : idouble) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : ireal) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : bool) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : char) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : dchar) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

template unboxNullable (T : wchar) {
   T unboxNullable (Box t) {return unboxNullablePrimitive!(T) (t);}
}

/**
 * Unbox a potentially null value replacing the null with supplied default.
 *
 * When the boxed value is null, the default is returned.
 */
template unboxNullDefault (T) {
   T unboxNullDefault (Box t, T def) {
      if (isNull (t))
         return def;
      else
         return unbox!(T)(t);
   }
}



struct NamedParameter {

   static NamedParameter opCall(T) (char[] name, T value) {
      
      // I have used a static if instead of static assert as the assert cannot
      // resolve name and value.name. I would prefer a compile time error.
      static if (is (T == NamedParameter)) {
         throw new SqlProgrammingException (
            "Invalid named parameter value. The value of a named parameter may "
            "not be a named parameter. Error on parameter (" ~ name ~
            ") trying to nest parameter (" ~ value.name ~
            ").");
      }

      Box v;

      static if (is (T == Box)) {
         /* Unnest the innermost value.
          */
         v = value;
         while (v.type == typeid (Box)) {
            v = unbox!(Box)(v);
         }
         
      } else {
         v = box (value);
      }

      NamedParameter tmp;
      tmp.m_name = name;
      tmp.m_value = v;
      
      return tmp;
   }


   char[] name () {
      return m_name;
   }

   
   Box value () {
      return m_value;
   }
   

   char[] toString () {
      return "(" ~ m_name ~ ": " ~ m_value.toString () ~ ")";
   }


   private char[] m_name;
   private Box    m_value;
}


public class BatchBuilderException : SqlInterfaceException {
   public this (char[] msg) {
      super (msg);
   }
}

public class BatchBuilder {

   public this () {
      this (10);
   }

   public this (int initialCapacity) {
      m_numValues       = -1;
      m_initialCapacity = initialCapacity;
      m_pos             = 0;
      
      m_batch = new Box[][m_initialCapacity];
   }

   public this (int initialCapacity, int numValues) {
      m_numValues       = numValues;
      m_initialCapacity = initialCapacity;
      m_pos             = 0;

      /* The inner array is not initialised as that array is built seperately.
       */
      m_batch = new Box[][m_initialCapacity];
   }


   /**
    * Append the supplied values to the batch. The method arguments are
    * converted to a values array and the append (Box[]) method is called.
    */
   public BatchBuilder append (...) {
      Box[] values = boxArray (_arguments, _argptr);
      return append (values);
   }


   /**
    * Append the supplied values array to the batch.
    */
   public BatchBuilder append (Box[] values) {

      if (values is null) {
         throw new BatchBuilderException (
            "Invalid values array supplied to append. Instead of a values "
            "array, the supplied reference is null.");
      }

      /* If m_numValues is not specified, then set with the length of the
       * first values array.
       *
       * When m_numValues has been set, subsequent calls to append must supply
       * exactly that number of values or an exception is thrown.
       */
      if (-1 == m_numValues) {
         m_numValues = values.length;

      } else if (m_numValues != values.length) {
         throw new BatchBuilderException (
            "Invalid number of values supplied to append. Expecting ("~
            str(m_numValues)~") values but received ("~str(values.length)~
            ") values.");
      }

      if (0 == values.length) {
         throw new BatchBuilderException (
            "Invalid number of values supplied to append. Zero values "
            "provided but at least one value must be provided.");
      }

      /* We do not validate the contents of the values array as this is done by
       * the executemany method.
       */

      /* Extend the array if the insert position is beyond the last element
       * of the array.
       */
      if (m_pos == m_batch.length) {
         m_batch.length = m_batch.length + m_initialCapacity;
      }

      /* Add the values array to the batch array.
       */
      m_batch[m_pos] = values;

      /* Move the insert point one over.
       */
      m_pos += 1;
      
      /* Allow for method chaining.
       */
      return this;
   }


   /**
    * Returns the populated slice of the underlying batch array.
    */
   public Box[][] toArray () {
      return m_batch[0..m_pos];
   }


   /**
    * The batch array contains one value array for each execution iteration.
    */
   protected Box[][] m_batch;

   /**
    * The initial size of the array and the increment size.
    */
   protected int m_initialCapacity;

   /**
    * Insertion position in the array.
    */
   protected int m_pos;

   /**
    * The expected number of values per execution. If a value array is added
    * with a number of values that does not match this one, an exception isNull
    * thrown.
    */
   protected int m_numValues;
}


/* Typed database row object.
 */
struct Row(T...) {
   /**
    * Column values stored as an expression tuple.
    */
   T c;

   /**
    * Flags a null column value. When true, the column at the corresponding
    * position is null.
    */
   bool[T.length] nullFlag;
}
